<?php
include_once BXD_INCLUDEPATH . 'lib/entity/Entity.php';
include_once BXD_INCLUDEPATH . 'lib/entity/EntityUtils.php';
include_once BXD_INCLUDEPATH . 'lib/entity/EntityType.php';

/**
 * Gestione delle entity legate alla base dati. Fornisce i metodi base per l'accesso ai dati
 *
 * @author Fabrizio Filieri / Damiano Ciarla
 * @version 2.0
 * @category lib/entity
 */
class EntityManager
{
	/**
	 * Costante per definire un valore NULL nelle query di INSERT e UPDATE
	 */
	const NULL_VALUE = "BXD_NULL_VALUE";

	protected $db = NULL;
	protected $entityUtils = NULL;

	public function __construct()
	{
		include_once BXD_INCLUDEPATH . 'lib/database/DBFactory.php';

		try
		{
			$this->db = DBFactory::getConnection();
		}
		catch(DatabaseConnectionErrorException $e)
		{
			echo $e->getTraceAsString();
		}

		$this->entityUtils = EntityUtils::getInstance();
	}

	/**
	 * Metodo per eseguire SELECT su una tabella con possibilità di effettuare tutte
	 * le combinazioni di interrogazioni semplicemente passando con parametro $pattern
	 * una entity avente valorizzati solamente i campi da inserire nella WHERE della query
	 *
	 * @param Entity $pattern Oggetto da cui prendere il pattern per la costruzione della query
	 * @param int $from Indice di partenza nella clausola LIMIT
	 * @param int $numItem Numero di valori da visualizzare nella LIMIT
	 * @param string $orderby Stringa che specifica l'ordinamento
	 *
	 * @example
	 *
	 *	Esempio di come utilizzare il metodo load per tirare fuori tutti gli utenti con nome 'Giuseppe'
	 *
	 *	$pattern = new Users();
	 *	$pattern->setName('Giuseppe');
	 *
	 *	try
	 *	{
	 *		$listUsers = EntityManagerFactory::get()->load($pattern);
	 *	}
	 *	catch(DatabaseErrorException $e)
	 *	{
	 *		echo $e;
	 *	}
	 *
	 * @return array Array di oggetti del medesimo tipo dell'oggetto contenuto nel PATTERN
	 */
	
	public function load($pattern, $from=null, $numItem=null, $orderby=null)
	{
		$result = array();

		$query  = "SELECT * ";
		$query .= "FROM " . $pattern->_getTableName() . " ";
		$query .= "WHERE ";

		foreach($pattern->_getVars() as $name => $col)
		{
			$value = $pattern->__get($name);
			
			if(isset($value))
				$query .= "`" . $col['DB_COLUMN'] . "` " . (($value === self::NULL_VALUE) ? "IS NULL" : "= :" . $col['DB_COLUMN']) . " AND ";
		}
		
		$query .= " 1 ";

		if(isset($orderby))
			$query .= "ORDER BY " . $orderby . " ";

		if(isset($from) && isset($numItem))
			$query .= "LIMIT " . $from . ", " . $numItem . " ";

		if($this->db->prepareQuery($query) !== FALSE)
		{
			foreach($pattern->_getVars() as $name => $col)
			{
				$value = $pattern->__get($name);
				
				if(isset($value) && $value !== self::NULL_VALUE)
				{
					switch($col['TYPE'])
					{
						case EntityType::BIT:
						case EntityType::TINYINT:
						case EntityType::SMALLINT:
						case EntityType::MEDIUMINT:
						case EntityType::INT:
						case EntityType::BIGINT:
						case EntityType::FLOAT:
						case EntityType::DOUBLE:
						case EntityType::DECIMAL:
							$this->db->bindParamInt($col['DB_COLUMN'], $value);
							break;

						case EntityType::CHAR:
						case EntityType::VARCHAR:
						case EntityType::TEXT:
						case EntityType::MEDIUMTEXT:
						case EntityType::LONGTEXT:
						case EntityType::ENUM:
							$this->db->bindParamString($col['DB_COLUMN'], $value);
							break;

						default:
							$this->db->bindParamGeneric($col['DB_COLUMN'], $value);
							break;
					}
				}
			}

			if($this->db->execute() !== FALSE)
			{
				$class_name = get_class($pattern);
				
				foreach($this->db->getFetchAll() as $item)
					$result[] = $this->entityUtils->fillEntity(new $class_name(), $item);
			}
			else
			{
				$error = $this->db->getLastError();
				BXD::newException("DatabaseError", $error[2]);
			}
		}
		else
		{
			$error = $this->db->getLastError();
			BXD::newException("DatabaseError", $error[2]);
		}

		return $result;
	}

	/**
	 * Ritorna la prima entity del risultato della ricerca. In caso di errore ritorna false.
	 *
	 * @param Entity $pattern l'entity sulla quale effettuare la ricerca
	 * @param  $from
	 * @param string $orderby Stringa che specifica l'ordinamento
	 * @return Entity l'entity del risultato della ricerca, false se non viene trovato alcun valore o
	 * in caso di errore.
	 */
	public function getOne($pattern, $orderby=null) 
	{
		$result = $this->load($pattern, 0, 1, $orderby);
		if (count($result)>0) // trovati risultati
			return $result[0];
		else
			return false;
	}

	public function count($pattern)
	{
		$result = array();

		$query  = "SELECT COUNT(*) as COUNT_ROW ";
		$query .= "FROM " . $pattern->_getTableName() . " ";
		$query .= "WHERE ";

		foreach($pattern->_getVars() as $name => $col)
		{
			$value = $pattern->__get($name);

			if(isset($value))
				$query .= "`" . $col['DB_COLUMN'] . "` " . (($value === self::NULL_VALUE) ? "IS NULL" : "= :" . $col['DB_COLUMN']) . " AND ";
		}
		
		$query .= " 1 ";

		if(isset($orderby))
			$query .= "ORDER BY " . $orderby . " ";

		if(isset($from) && isset($numItem))
			$query .= "LIMIT " . $from . ", " . $numItem . " ";

		if($this->db->prepareQuery($query) !== FALSE)
		{
			foreach($pattern->_getVars() as $name => $col)
			{
				$value = $pattern->__get($name);
				
				if(isset($value) && $value !== self::NULL_VALUE)
				{
					switch($col['TYPE'])
					{
						case EntityType::BIT:
						case EntityType::TINYINT:
						case EntityType::SMALLINT:
						case EntityType::MEDIUMINT:
						case EntityType::INT:
						case EntityType::BIGINT:
						case EntityType::FLOAT:
						case EntityType::DOUBLE:
						case EntityType::DECIMAL:
							$this->db->bindParamInt($col['DB_COLUMN'], $value);
							break;

						case EntityType::CHAR:
						case EntityType::VARCHAR:
						case EntityType::TEXT:
						case EntityType::MEDIUMTEXT:
						case EntityType::LONGTEXT:
						case EntityType::ENUM:
							$this->db->bindParamString($col['DB_COLUMN'], $value);
							break;

						default:
							$this->db->bindParamGeneric($col['DB_COLUMN'], $value);
							break;
					}
				}
			}

			if($this->db->execute() !== FALSE)
			{
				$class_name = get_class($pattern);
				$result = $this->db->getArrayAssocResult();
				$result = $result['COUNT_ROW'];
			}
			else
			{
				$error = $this->db->getLastError();
				BXD::newException("DatabaseError", $error[2]);
			}
		}
		else
		{
			$error = $this->db->getLastError();
			BXD::newException("DatabaseError", $error[2]);
		}

		return $result;
	}

	/**
	 * Metodo che esegue una INSERT nel Database prendendo in ingresso un oggetto ENTITY
	 * sul quale viene costruita la query servendosi solamente dei campi valorizzati e
	 * ignorando quelli NULL. Una volta effettuata l'operazione di INSERT il metodo si
	 * occupa di valorizzare il campo ID (se esiste ed è autoincrement) con l'ultimo
	 * inserito nella tabella
	 *
	 * @param Entity $entity Oggetto Entity da inserire nel DB
	 *
	 * @example
	 * 
	 *	Esempio di come utilizzare il metodo insert per aggiungere una semplice entity
	 *	NAME-SURNAME nella tabella USERS
	 *
	 *	$EntityUsers = new EntityUsers();
	 *	$EntityUsers->setName('Giuseppe');
	 *	$EntityUsers->setSurname('Rossi');
	 *
	 *	try
	 *	{
	 *		EntityManagerFactory::get()->insert($EntityUsers);
	 *	}
	 *	catch(DatabaseErrorException $e)
	 *	{
	 *		echo $e;
	 *	}
	 */

	public function insert($entity, $useIgnoreMethod = FALSE)
	{
		$field = "";
		$value = "";
		$index = 0;

		foreach($entity->_getVars() as $name => $col)
		{
			$val = $entity->__get($name);

			if(isset($val))
			{
				$field .= (($index > 0) ? ", `" : "`") . $col['DB_COLUMN'] . "`";
				$value .= (($index > 0) ? ", :" : ":") . $col['DB_COLUMN'];

				$index++;
			}
		}

		$query  = "INSERT " . (($useIgnoreMethod) ? " IGNORE " : "") . " INTO " . $entity->_getTableName() . " ";
		$query .= "(" . $field . ") VALUES (" . $value . ")";

		if($this->db->prepareQuery($query) !== FALSE)
		{
			foreach($entity->_getVars() as $name => $col)
			{
				$val = $entity->__get($name);

				if(isset($val) || $val === self::NULL_VALUE)
				{
					switch($col['TYPE'])
					{
						case EntityType::BIT:
						case EntityType::TINYINT:
						case EntityType::SMALLINT:
						case EntityType::MEDIUMINT:
						case EntityType::INT:
						case EntityType::BIGINT:
						case EntityType::FLOAT:
						case EntityType::DOUBLE:
						case EntityType::DECIMAL:
							$this->db->bindParamInt($col['DB_COLUMN'], (($val === self::NULL_VALUE) ? NULL : $val));
							break;

						case EntityType::CHAR:
						case EntityType::VARCHAR:
						case EntityType::TEXT:
						case EntityType::MEDIUMTEXT:
						case EntityType::LONGTEXT:
						case EntityType::ENUM:
							$this->db->bindParamString($col['DB_COLUMN'], (($val === self::NULL_VALUE) ? NULL : $val));
							break;

						default:
							$this->db->bindParamGeneric($col['DB_COLUMN'], (($val === self::NULL_VALUE) ? NULL : $val));
							break;
					}
				}
			}

			if($this->db->execute() === FALSE)
			{
				$error = $this->db->getLastError();
				BXD::newException("DatabaseError", $error[2]);
			}
		}
		else
		{
			$error = $this->db->getLastError();
			BXD::newException("DatabaseError", $error[2]);
		}

		// Riempimento serial
		$serial = $entity->_getSerial();

		if($serial !== NULL)
		{
			$entity->__set($serial['VAR_NAME'], $this->db->getLastInsertId());
		}
	}

	public function save($entity, $pattern = null)
	{
		
		$query = "UPDATE " . (($pattern !== NULL) ? $pattern->_getTableName() : $entity->_getTableName()) . " SET ";

		$count = 0;

		foreach($entity->_getVars() as $name => $col)
		{
			$v = $entity->__get($name);

			if(isset($v) || $v === self::NULL_VALUE)
			{
				if($count == 0)
				{
					$query .= "`" . $col['DB_COLUMN'] . "` = :entity_" . $col['DB_COLUMN'];
				}
				else
				{
					$query .= ", `" . $col['DB_COLUMN'] . "` = :entity_" . $col['DB_COLUMN'];
				}

				$count++;
			}
		}

		if($count == 0)
			return;

		$query .= " WHERE ";

		$serial = $entity->_getSerial();

		if($serial !== NULL && $pattern === NULL) // esiste una chiave primaria autoincrement
		{
			$query .= "`" . $serial["DB_COLUMN"] . "` = :" . $serial["DB_COLUMN"];
		}
		else if(isset($pattern)) // usiamo il pattern per il riconoscimento delle tuple da aggiornare
		{
			$count = 0;

			foreach($pattern->_getVars() as $name => $col)
			{
				$v = $pattern->__get($name);

				if(isset($v))
				{
					if($count == 0)
					{
						$query .= "`" . $col['DB_COLUMN'] . "` " . (($v === self::NULL_VALUE) ? "IS NULL" : "= :pattern_" . $col['DB_COLUMN']);
					}
					else
					{
						$query .= " AND `" . $col['DB_COLUMN'] . "` " . (($v === self::NULL_VALUE) ? "IS NULL" : "= :pattern_" . $col['DB_COLUMN']);
					}

					$count++;
				}
			}

			if ($count == 0)
				$query .= " 1 ";
		}
		else // impossibile aggiornare senza informazioni...
		{
			throw new Exception("SERIAL not found or empty pattern");
		}

		if($this->db->prepareQuery($query) !== FALSE)
		{
			// binding entity
			foreach($entity->_getVars() as $name => $col)
			{
				$v = $entity->__get($name);
				
				if(isset($v) || $v === self::NULL_VALUE)
				{
					switch($col['TYPE'])
					{
						case EntityType::BIT:
						case EntityType::TINYINT:
						case EntityType::SMALLINT:
						case EntityType::MEDIUMINT:
						case EntityType::INT:
						case EntityType::BIGINT:
						case EntityType::FLOAT:
						case EntityType::DOUBLE:
						case EntityType::DECIMAL:
							$this->db->bindParamInt("entity_" . $col['DB_COLUMN'], (($v === self::NULL_VALUE) ? NULL : $v));
							break;

						case EntityType::CHAR:
						case EntityType::VARCHAR:
						case EntityType::TEXT:
						case EntityType::MEDIUMTEXT:
						case EntityType::LONGTEXT:
						case EntityType::ENUM:
							$this->db->bindParamString("entity_" . $col['DB_COLUMN'], (($v === self::NULL_VALUE) ? NULL : $v));
							break;

						default:
							$this->db->bindParamGeneric("entity_" . $col['DB_COLUMN'], (($v === self::NULL_VALUE) ? NULL : $v));
							break;
					}
				}
			}

			if($serial !== NULL && $pattern === NULL) // esiste una chiave primaria autoincrement
			{
				$this->db->bindParamInt($serial["DB_COLUMN"], $entity->__get($serial["VAR_NAME"]));
			}
			else
			{
				// binding pattern
				foreach($pattern->_getVars() as $name => $col)
				{
					$v = $pattern->__get($name);
					if(isset($v))
					{
						switch($col['TYPE'])
						{
							case EntityType::BIT:
							case EntityType::TINYINT:
							case EntityType::SMALLINT:
							case EntityType::MEDIUMINT:
							case EntityType::INT:
							case EntityType::BIGINT:
							case EntityType::FLOAT:
							case EntityType::DOUBLE:
							case EntityType::DECIMAL:
								if($v !== self::NULL_VALUE)
									$this->db->bindParamInt("pattern_" . $col['DB_COLUMN'], $v);
								break;

							case EntityType::CHAR:
							case EntityType::VARCHAR:
							case EntityType::TEXT:
							case EntityType::MEDIUMTEXT:
							case EntityType::LONGTEXT:
							case EntityType::ENUM:
								if($v !== self::NULL_VALUE)
									$this->db->bindParamString("pattern_" . $col['DB_COLUMN'], $v);
								break;

							default:
								if($v !== self::NULL_VALUE)
									$this->db->bindParamGeneric("pattern_" . $col['DB_COLUMN'], $v);
								break;
						}
					}
				}
			}

			if($this->db->execute() === FALSE)
			{
				$error = $this->db->getLastError();
				BXD::newException("DatabaseError", $error[2]);
			}

			return $this->db->getNumRows();
		}
		else
		{
			$error = $this->db->getLastError();
			BXD::newException("DatabaseError", $error[2]);
		}
	}

	public function delete($pattern)
	{
		$result = array();

		$query  = "DELETE ";
		$query .= "FROM " . $pattern->_getTableName() . " ";
		$query .= "WHERE ";

		$count = 0;
		foreach($pattern->_getVars() as $name => $col)
		{
			$v = $pattern->__get($name);
			if(isset($v))
			{
				$query .= "`" . $col['DB_COLUMN'] . "` = :" . $col['DB_COLUMN'] . " AND ";
				$count++;
			}
		}
		$query .= " 1 ";

		if($count == 0)
			return;

		if($this->db->prepareQuery($query) !== FALSE)
		{
			foreach($pattern->_getVars() as $name => $col)
			{
				$v = $pattern->__get($name);
				if(isset($v))
				{
					switch($col['TYPE'])
					{
						case EntityType::BIT:
						case EntityType::TINYINT:
						case EntityType::SMALLINT:
						case EntityType::MEDIUMINT:
						case EntityType::INT:
						case EntityType::BIGINT:
						case EntityType::FLOAT:
						case EntityType::DOUBLE:
						case EntityType::DECIMAL:
							$this->db->bindParamInt($col['DB_COLUMN'], $v);
							break;

						case EntityType::CHAR:
						case EntityType::VARCHAR:
						case EntityType::TEXT:
						case EntityType::MEDIUMTEXT:
						case EntityType::LONGTEXT:
						case EntityType::ENUM:
							$this->db->bindParamString($col['DB_COLUMN'], $v);
							break;

						default:
							$this->db->bindParamGeneric($col['DB_COLUMN'], $v);
							break;
					}
				}
			}

			if($this->db->execute() === FALSE)
			{
				$error = $this->db->getLastError();
				BXD::newException("DatabaseError", $error[2]);
			}
		}
		else
		{
			$error = $this->db->getLastError();
			BXD::newException("DatabaseError", $error[2]);
		}

		return $result;
	}

	public function oneToMany($from, $entityTo, $fromItem, $toItem)
	{
		$manager = EntityManagerFactory::get();
		$to = new $entityTo();

		for($i = 0; $i < count($fromItem); $i++)
		{
			$to->__set($toItem[$i], $from->__get($fromItem[$i]));
		}
		return $manager->load($to);
	}

	public function oneToOne($from, $entityTo, $fromItem, $toItem)
	{
		$manager = EntityManagerFactory::get();
		$to = new $entityTo();

		for($i = 0; $i < count($fromItem); $i++)
		{
			$to->__set($toItem[$i], $from->__get($fromItem[$i]));
		}
		$res = $manager->load($to);
		return $res[0];
	}
}
?>
